Accessibilité (a11y) et Internationalisation (i18n)
Accessibilité (a11y)
Concepts clés
- Sémantique : fournir du contexte pour lecteurs d'écran
- Contraste : texte lisible sur fond
- Taille de police flexible : supporter les paramètres du système
- Navigation clavier : être navigable sans souris/touch
- Gestes alternatifs : ne pas dépendre d'un seul geste
Sémantique
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
void onAdd() {
debugPrint('Ajout au panier');
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Accessibilité: Semantics')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Semantics(
label: 'Bouton pour ajouter au panier',
button: true,
child: IconButton(
onPressed: onAdd,
icon: const Icon(Icons.add_shopping_cart),
tooltip: 'Ajouter au panier',
),
),
const SizedBox(height: 12),
IconButton(
onPressed: onAdd,
icon: const Icon(Icons.add_shopping_cart),
tooltip: 'Ajouter au panier', // Lecteur d'écran le lira
),
],
),
),
),
);
}
}
Images accessibles
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Image accessible')),
body: Center(
child: Image.asset(
'assets/logo.png',
semanticLabel: 'Logo de l\'application',
),
),
),
);
}
}
Texte adaptatif à la taille de l'appareil
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Texte adaptatif')),
body: Builder(
builder: (context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Mon texte',
style: TextStyle(
fontSize: MediaQuery.of(context).textScaleFactor * 16,
),
),
const SizedBox(height: 12),
Text(
'Mon texte',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
},
),
),
);
}
}
Focus et navigation clavier
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Navigation clavier')),
body: Center(
child: Focus(
onKey: (node, event) {
if (event.isKeyPressed(LogicalKeyboardKey.enter)) {
debugPrint('Entrée pressée');
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: ElevatedButton(
onPressed: () {},
child: const Text('Bouton'),
),
),
),
),
);
}
}
Tests accessibilité rapides
- VoiceOver (iOS) / TalkBack (Android) activés : vérifier que tout est annoncé
- Taille police x1.5/x2 : vérifier overflow
- Mode contrasté : vérifier lisibilité
- Navigation clavier : utiliser Tab/Shift+Tab
Internationalisation (i18n)
Pourquoi i18n ?
- Supporter plusieurs langues
- Adapter formats (dates, devises, nombres)
- Localisation des contenus
Setup avec flutter_gen_l10n
1. Configuration dans pubspec.yaml
flutter_localizations:
sdk: flutter
2. Créer fichiers ARB
lib/l10n/app_en.arb
lib/l10n/app_fr_CA.arb
lib/l10n/app_es.arb
app_en.arb :
{
"helloWorld": "Hello World",
"welcome": "Welcome {name}",
"@welcome": {
"description": "Accueil avec le nom"
}
}
app_fr_CA.arb :
{
"helloWorld": "Bonjour le monde",
"welcome": "Bienvenue {name}"
}
3. Générer locales dans pubspec.yaml
flutter:
generate: true
localization:
generate: true
enable-asserts: true
Lancer flutter gen-l10n pour générer les fichiers
4. Configuration de MaterialApp
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('fr', 'CA'), // Ou null pour suivre le système
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('i18n')),
body: Center(
child: Text(AppLocalizations.of(context)!.helloWorld),
),
);
}
}
5. Utiliser dans les widgets
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: const Text('Textes localisés')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.helloWorld),
const SizedBox(height: 8),
Text(l10n.welcome(name: 'Alice')),
],
),
),
);
}
}
Formats localisés (dates, devises, nombres)
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
final date = DateFormat.yMMMd('fr_CA').format(DateTime.now());
final currency = NumberFormat.simpleCurrency(locale: 'fr_CA').format(99.99);
final number = NumberFormat('#,##0.##', 'fr_CA').format(1234.567);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Formats localisés')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Date: $date'),
Text('Devise: $currency'),
Text('Nombre: $number'),
],
),
),
),
);
}
}
Changement de langue en temps réel
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Locale _locale = const Locale('fr', 'CA');
void setLocale(Locale locale) {
setState(() => _locale = locale);
}
Widget build(BuildContext context) {
return MaterialApp(
locale: _locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: HomePage(onLocaleChange: setLocale),
);
}
}
class HomePage extends StatelessWidget {
final void Function(Locale) onLocaleChange;
const HomePage({super.key, required this.onLocaleChange});
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: const Text('Changer la langue')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.helloWorld),
const SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => onLocaleChange(const Locale('fr', 'CA')),
child: const Text('FR-CA'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => onLocaleChange(const Locale('en')),
child: const Text('EN'),
),
],
),
],
),
),
);
}
}
Bonnes pratiques
Accessibilité
- Toujours ajouter des labels : Semantics, Tooltip, semanticLabel
- Vérifier les contrastes : utiliser ColorScheme
- Tester avec lecteur d'écran : VoiceOver/TalkBack
- Support clavier : toutes les actions tactiles doivent être accessibles au clavier
- Tailles adaptatrices : respecter les paramètres de taille système
Internationalisation
- Utiliser ARB : format standard et facile à externaliser
- Pas de chaînes en dur : toujours via AppLocalizations
- Tester plusieurs locales : pas toutes les langues = même longueur
- Formats localisés : dates, devises, nombres
- RTL support : tester langues de droite à gauche (arabe, hébreu)